/*
 * Copyright 2009 Zero Separation
 *
 *     This file is part of PDSUtilities.
 *
 *  PDSUtilities is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  PDSUtilities is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with PDSUtilities.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package com.zero_separation.pds.utilities.indexingmaps;

import com.zero_separation.pds.utilities.referencecollections.ReferenceArrayList;
import com.sun.sgs.app.AppContext;
import com.sun.sgs.app.ManagedObjectRemoval;
import com.sun.sgs.app.ManagedReference;
import com.sun.sgs.app.util.ScalableHashMap;
import com.zero_separation.pds.utilities.HasId;
import java.math.BigInteger;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;

/**
 * Provides access to indexed data in a ScalableIndexingMap.
 *
 * <p>A new implementation of this abstract class should be created for each
 * field being indexed. The getKey method must be overridden to return the
 * correct IndexableField<> from the passed in Value but otherwise no changes
 * should be made to this class. Once the MultivalueIndex has been created and added
 * to a ScalableIndexingMap the query functions can be used to access data.
 *
 * <p>The query functions must not be used before the UniqueIndex has been added
 * to a map or after the map has been deleted. In addition each UniqueIndex object
 * can only be added to one map and attempts to add it to a second will result
 * in an IllegalStateException being thrown.
 *
 * @param <K> The data type to use for keys in this index
 * @param <V> The data type being mapped to as values in this index
 */
public abstract class MultivalueIndex< K, V extends HasId > extends Index< V > implements ManagedObjectRemoval {

    /** The version of the serialized form of this class. */
    private static final long serialVersionUID = 1L;
    
    private ManagedReference<InternalData<K, V>> mapRef = null;

    public void removingObject() {
        if (mapRef != null)
            AppContext.getDataManager().removeObject(mapRef.get());
    }

    /**
     * For a multi-value index all insertions are valid.
     * 
     * @param value
     * @throws com.zeroseparation.pds.utilities.indexingmaps.KeyAlreadyInIndexException
     */
    void validInsertion(V value) throws KeyAlreadyInIndexException {
        return;
    }

    /**
     * Must be overriden to return the correct IndexableField object for the
     * passed value.
     *
     * <p>This is the only method that needs overriding to create a new UniqueIndex.
     * This method must return the same IndexableField object each time it is
     * called.
     *
     * @param value The value from which to extract the IndexableField.
     * @return The IndexableField for this value.
     */
    public abstract Indexable<K> getKey( V value );

    /**
     * Creates the map backing this index and passes it back to the calling
     * ScalableIndexingMap. Updates our own reference to the map.
     *
     * @return The new internal map.
     */
    void createInternalData( ) {
        if (mapRef != null) {
            throw new IllegalStateException("Attempting to add index to multiple maps");
        }
        InternalData< K, V > map = new InternalData< K, V >( this );
        mapRef = AppContext.getDataManager().createReference(map);
    }

    /**
     * Returns the value stored in the ScalableIndexingMap which has its
     * IndexableField corresponding to this UniqueIndex set to the passed value.
     *
     * @param key The key to search for in this index
     * @return The value matching this key or null if no matching value is found.
     */
    public List<V> get( final K key ) {
        return mapRef.get().get(key);
    }

    /**
     * Returns the number of elements stored in this index.
     *
     * @return The number of elements.
     */
    public int size() {
        return mapRef.get().size();
    }

    /**
     * Returns whether the index is empty or not.
     *
     * @return True if the index contains no elements
     */
    public boolean isEmpty() {
        return mapRef.get().isEmpty();
    }

    /**
     * Returns whether the index contains the specified key or not.
     *
     * @param key The key to check for.
     * @return True if the index contains the key.
     */
    public boolean containsKey(K key) {
        return mapRef.get().containsKey(key);
    }

    /**
     * Returns whether the index contains the specified value or not.
     *
     * @param value The value to check for.
     * @return True if the index contains the value.
     */
    public boolean containsValue(V value) {
        return mapRef.get().containsValue(value);
    }

    /**
     * Returns a Set listing all of the keys in this index.
     *
     * <p>The returned Set is unmodifiable and cannot be changed as no
     * changes can be made to the mapping through the indexes. Changes must
     * be made directly on the map itself.
     *
     * @return A set containing all the keys in this index.
     */
    public Set<K> getKeys() {
        return Collections.unmodifiableSet(mapRef.get().keySet());
    }

    /**
     * Returns a Collection listing all of the values in this index.
     *
     * <p>The returned Collection is unmodifiable and cannot be changed as no
     * changes can be made to the mapping through the indexes. Changes must
     * be made directly on the map itself.
     *
     * @return A set containing all the values in this index.
     */
    public Collection<V> getValues() {
        return mapRef.get().getValues();
    }

    void put(final V value) throws KeyAlreadyInIndexException {
        mapRef.get().put(value);
    }

    void remove(final V value) {
        mapRef.get().remove(value);
    }
    
    void clear() {
        mapRef.get().clear();
    }
    
    /**
     * This class is used internally to wrap a ScalableHashMap and provide the
     * actual indexing functionality.
     *
     * @param <K> The type of the Key used for this index.
     * @param <V> The type of the Value used for this index.
     */
    private static class InternalData< K, V extends HasId> extends ScalableHashMap< K, List<V>> implements FieldChangeListener<K> {

        private final ManagedReference<MultivalueIndex< K, V >> accessor;

        /**
         * InternalData classes are only meaningful when paired with an accessor.
         *
         * @param accessor The accessor used for this index.
         */
        InternalData( final MultivalueIndex< K, V > accessor )
        {
            super();
            this.accessor = AppContext.getDataManager().createReference(accessor);
        }

        /**
         * The listener for field changed events which updates this index.
         *
         * @param oldkey The old value for the key
         * @param newkey The new value for the key
         * @throws com.zeroseparation.testserver.indexedmaps.mk2.ScalableIndexingMap.KeyAlreadyInIndexException
         */
        public void fieldChanged(BigInteger parentId, K oldkey, K newkey) throws KeyAlreadyInIndexException {

            List<V> oldValues = get(oldkey);
            List<V> newValues;

            if (containsKey(newkey)) {
                newValues = get(newkey);
            } else
                newValues = new ReferenceArrayList<V>();

            if (oldValues.size()==1) {
                remove(oldkey);
                newValues.add(oldValues.get(0));
                put(newkey, newValues);
            }
            else {
                V value = null;
                for (V v: oldValues)
                    if (v.getId().equals(parentId)) {
                        value = v;
                        break;
                    }

                if (value == null)
                    throw new IllegalStateException("Processing field changed for Key "+oldkey+" to "+newkey+" but object "+parentId+" not found");

                oldValues.remove(value);
                put(oldkey, oldValues);

                newValues.add(value);
                put(newkey, newValues);
            }
        }

        /**
         * Used to add a new value to the index. It reads the key from the value
         * and adds it to the underlying ScalableHashMap. It also adds a listener
         * to the IndexableField object so we are notified of changes.
         *
         * @param value The value to add.
         * @throws com.zeroseparation.testserver.indexedmaps.mk2.ScalableIndexingMap.KeyAlreadyInIndexException
         */
        public void put(final V value) throws KeyAlreadyInIndexException {
            Indexable<K> field = accessor.get().getKey(value);
            List<V> values;

            if (containsKey(field.get())) {
                values = get(field.get());
            } else
                values = new ReferenceArrayList<V>();

            values.add(value);

            put( field.get(), values );
            field.addListener(this);
        }


        /**
         * Used to remove a value from the index. It reads the key from the value
         * and removes it from the underlying ScalableHashMap. It also removes the
         * listener from the IndexableField object.
         */
        public void remove(final V value) {
            Indexable<K> field = accessor.get().getKey(value);
            List<V> values = get(field.get());

            if (values.size()==1) {
                remove(field.get());
            }
            else {
                values.remove(value);
                put(field.get(), values);
            }
            field.removeListener(this);
        }

        /**
         * Uses the accessor to get the key from the value object.
         *
         * @param value The value object to get the key from.
         * @return The key used for this index for the given value object.
         */
        public K getKey(final V value) {
            return accessor.get().getKey(value).get();
        }

        /**
         * Clear is overridden to ensure that our change listeners are removed
         * from every IndexableField being monitored.
         */
        @Override
        public void clear() {
            for (V value: getValues()) {
                Indexable<K> field = accessor.get().getKey(value);
                accessor.get().getKey(value).removeListener(this);
            }
            super.clear();
        }

        public Collection<V> getValues() {
            Collection<List<V>> values = values();
            List<V> list = new ReferenceArrayList<V>(values.size());
            for (List<V> v: values)
                list.addAll(v);
            return Collections.unmodifiableCollection(list);
        }

        @Override
        public int size() {
            int size = 0;
            for (List<V> v: values())
                size += v.size();
            return size;
        }

        public boolean validInsertion(K key) {
            return true;
        }

    }
}
